/*
 * Created on 21/11/2009
 *
 * To change the template for this generated file go to
 * Window&gt;Preferences&gt;Java&gt;Code Generation&gt;Code and Comments
 */
package org.jma.app.ewisdom.services;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Vector;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.store.LockObtainFailedException;
import org.jma.app.ewisdom.entities.Content;
import org.jma.app.ewisdom.entities.EntityWithACL;
import org.jma.app.ewisdom.entities.FileDescriptor;
import org.jma.app.ewisdom.entities.FilePart;
import org.jma.app.ewisdom.entities.FileLocator;
import org.jma.app.ewisdom.entities.User;
import org.jma.app.ewisdom.exceptions.ContentException;
import org.jma.app.ewisdom.exceptions.FileException;
import org.jma.app.ewisdom.exceptions.UserException;
import org.jma.app.ewisdom.services.utils.EwisdomDownloader;
import org.jma.app.ewisdom.services.utils.EwisdomUploader;
import org.jma.app.ewisdom.services.utils.FileJoiner;
import org.jma.app.ewisdom.services.utils.FileSplitter;
import org.jma.app.ewisdom.services.utils.LuceneIndexer;
import org.jma.app.ewisdom.services.utils.ScoreDocs;
import org.jma.app.ewisdom.services.utils.queries.GlobalQuery;
import org.jma.app.ewisdom.services.utils.queries.QueryReceiver;
import org.jma.app.ewisdom.services.utils.queries.QueryVector;
import org.jma.lib.soap.SOAPProxy;
import org.jma.lib.soap.sessions.SOAPSession;
import org.jma.lib.soap.utils.XMLSerializable;
import org.jma.lib.utils.http.FileDownloadedEvent;
import org.jma.lib.utils.http.FileDownloadedListener;
import org.jma.lib.utils.http.FileDownloader;
import org.jma.lib.utils.http.FileUploadedEvent;
import org.jma.lib.utils.http.FileUploadedListener;
import org.jma.lib.utils.networking.Ping;

import rice.p2p.commonapi.NodeHandle;
import rice.pastry.messaging.Message;

import easypastry.cast.CastContent;
import easypastry.cast.CastHandler;
import easypastry.cast.CastListener;
import easypastry.core.PastryKernel;
import easypastry.dht.DHTException;
import easypastry.dht.DHTHandler;
import easypastry.util.Utilities;

/**
 * SrvFileManager class allows the access to the Files linked in contents. It publishes
 * methods as services described below.<br>
 * @author Jesus Mari, jesmari@ono.com
 * @version 1.0
 * @since 1.0
 */
public class SrvFileManager implements 	FileUploadedListener, 
										FileDownloadedListener{
	public static String REPOSITORY;			/**Folder where the structure of index folders and files will be created"*/ 
	public static int SERVER_PORT=0;			/**Server port that server the files */
	public static int FIRST_AVAILABLE_PORT;		/**First Port available to upload a file*/ 
	public static String HTTP_SERVER_FOLDER;	/**Root adress of the http server*/
	public static final int AVAILABLE_PORTS=100;/**number of ports to upload files*/
	public static final int CHUNK_SIZE=256;		/**size of chunks(256Kbytes)*/
	
	public static final String MESSAGING_GROUP="p2p://org.jma.app.ewisdom";
	public static Runnable[] uploaders = new Runnable[AVAILABLE_PORTS]; /**to store thread references*/
	public static Runnable[] downloaders=new Runnable[AVAILABLE_PORTS]; /**to store thread references*/
	
	

	/**
	 * checks if the entity has the pair (userName,role) if not, it will throw a ContentException
	 * @param userName
	 * @param content
	 * @param role
	 * @throws ContentException
	 * @throws UserException
	 */	
	private void checkRoules(String userName,EntityWithACL acl,String roule) throws FileException, UserException{
		if (acl==null) return;
		SrvUserManager sum = new SrvUserManager();
		HashMap soapParams= new HashMap();
		soapParams.put("userName", userName);
		User userObject=  sum.getUserByUserName(soapParams);
		Vector groups = userObject.getGroups();
		boolean authorized = false;
		
		
		if (acl.isInACL(userName,roule)){
		}else{
			if (groups==null) throw new FileException("Insufficient privileges");				
			for (int i =0;i<groups.size();i++){
				if (acl.isInACL((String)groups.get(i),"CHANGE_CONTENT_ROULES")){
					authorized = true;
					break;
				}
			}
			if (!authorized) throw new FileException("Insufficient privileges");
		}
	
	}
	
	
	/**
	 * Adds a new FileDescriptor to a content
	 * @param soapParams <code>CID</code> parent content, <code>title</code> of the file, <code>pos</code> position.
	 * @return a fileLocator where we can upload the file.
	 * @throws UserException
	 * @throws ContentException
	 * @throws DHTException
	 * @throws FileException
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws CloneNotSupportedException
	 */
	public XMLSerializable addFile(HashMap soapParams) throws 
												UserException, 
												ContentException, 
												DHTException, 
												FileException, 
												IOException, 
												InterruptedException, CloneNotSupportedException{
		String title=(String)soapParams.get("title");
		String cid=(String)soapParams.get("CID");		
		String pos=(String)soapParams.get("pos");

		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		SrvContentManager scm=new SrvContentManager();
		Content ct=scm.getContent(soapParams); //parameter CID
		
		checkRoules(userName,ct,"ADD_CONTENTS");
		
		FileDescriptor fd;
		if ((fd=(FileDescriptor)dhtFiles.get(FileDescriptor.calculateHashCode(ct.getPath()+"/"+title)))==null){ 
			fd=ct.addFile(Integer.parseInt(pos), title);
			fd.setOwner(userName);
		}
		scm.updateContent(ct);
		
		if (ct.getParent().equals("null")){
			scm.removeFromContentsIndex(ct);
			scm.addToContentsIndex(ct);
		}
		
		// Uses FilePartLocator to return name and server port where
		// client can send its file.
		FileLocator fl=new FileLocator();
		fl.setHostName(getLocalAddress());
		fl.setUrl("");
		int p=0;
		// Creates a new thread that listens the port in order to 
		// upload a file.
		// this thread maintains the reference to a FileDescriptor and 
		// updates the FileParts field.
		if (( p=createThreadToUpload(fd))<0) throw new FileException("No more ports availiables");
		fl.setPort(p);
		dhtFiles.put(""+fd.hashCode(),fd);	
		return fl;
	}
	
	/**
	 * 
	 * @param soapParams <code>FID</code> identification of a fileDescriptor, <code>version</code> name of a version "ver_x"
	 * @return fileLocator where we can download the file
	 * @throws UserException
	 * @throws ContentException
	 * @throws DHTException
	 * @throws FileException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public XMLSerializable getFile(HashMap soapParams) throws 	UserException, 
																ContentException, 
																DHTException, 
																FileException, 
																IOException,
																InterruptedException{
		String fid=(String)soapParams.get("FID");
		String ver = (String)soapParams.get("version");
		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

		FileDescriptor fd=(FileDescriptor)dhtFiles.get(fid); //param title
		
		checkRoules(userName,fd,"READ_CONTENTS");
		FilePart[]fp=fd.getFileParts(ver);
		FileLocator mainLoc=fd.getMainNode();
		if (!fileExists(fd)) {
			for (int i=0;i<fp.length;i++){
				FileLocator chossedFl=chooseAServer(fp[i]);
				createThreadToDownload(fd,fp[i],chossedFl);
			}
			// it will download the file but till then,it sends the main node 
			// to the client.
			return mainLoc;
		}
		
		FileLocator fploc = new FileLocator();
		String host=getLocalAddress();
		fploc.setHostName(host);
		fploc.setPort(SERVER_PORT);
		fploc.setUrl("http://"+host+":"+SERVER_PORT+HTTP_SERVER_FOLDER+fd.getPath()+"/"+fd.getFileName());
		return fploc;
	}
	
	
	
	/**
	 * removes a FileDescriptor
	 * @param soapParams <code>FID</code> identification of a fileDescriptor
	 * @return the removed FileDescriptor
	 * @throws UserException
	 * @throws ContentException
	 * @throws DHTException
	 * @throws FileException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public XMLSerializable removeFile(HashMap soapParams) throws 	UserException, 
																	ContentException, 
																	DHTException, 
																	FileException, 
																	IOException,
																	InterruptedException{
		String fid=(String)soapParams.get("FID");
		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

		FileDescriptor fd=(FileDescriptor)dhtFiles.get(fid); //param title

		checkRoules(userName,fd,"REMOVE_CONTENTS");
		dhtFiles.remove(fid);
		return fd;
	}
	
	
	
	/**
	 * Initiates a query over the documentation 
	 * @param soapParams <code>query</code> string of a query, <code>mode</code>, if mode is equal to "global"
	 * it ask for the query to every node connected to the p2p ring and you will need to update the 
	 * query result with the service getQueryResults. It uses a {@link QueryReceiver} that is initiated on the session
	 * creation (sessionInitiated on Ewisdom class).
	 * @return a vector of queries
	 * @throws UserException
	 * @throws DHTException
	 * @throws FileException
	 * @throws CorruptIndexException
	 * @throws LockObtainFailedException
	 * @throws IOException
	 * @throws ParseException
	 */
	public XMLSerializable doQuery(HashMap soapParams) throws 	UserException, 
																			DHTException, 
																			FileException, 
																			CorruptIndexException, 
																			LockObtainFailedException, 
																			IOException, ParseException
	{
		
		String query=(String)soapParams.get("query");
		String mode=(String)soapParams.get("mode");
		
		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));
		if (!mode.equals("global")){
			LuceneIndexer lind=new LuceneIndexer();
			ScoreDocs result=(ScoreDocs)lind.doQuery(query);
			QueryVector queryVector =new QueryVector();
			queryVector.add(result);
			return queryVector;// it puts the result inside the vector to unifier the returned object.
		}else{
			CastHandler cast = PastryKernel.getCastHandler();
			GlobalQuery gq=new GlobalQuery(MESSAGING_GROUP);
			gq.setQuery(query);
			gq.setSource(cast.getLocalNodeHandle());
            cast.subscribe(MESSAGING_GROUP);
            cast.sendMulticast(MESSAGING_GROUP, gq);
            return getQueryResults(soapParams);
		}
		
	}	
	
	/**
	 * Gets the query result stored in the queryReceiver object of your session. 
	 * @param soapParams <code>query</code> string of a query, It uses a {@link QueryReceiver} that is initiated on the session
	 * creation (sessionInitiated on Ewisdom class).
	 * @return a vector of queries
	 * @throws UserException
	 * @throws DHTException
	 * @throws FileException
	 * @throws CorruptIndexException
	 * @throws LockObtainFailedException
	 * @throws IOException
	 * @throws ParseException
	 */
	public XMLSerializable getQueryResults(HashMap soapParams) throws 	UserException, 
																		DHTException, 
																		FileException, 
																		CorruptIndexException, 
																		LockObtainFailedException, 
																		IOException, ParseException{
	
		String query=(String)soapParams.get("query");
		QueryReceiver qr = (QueryReceiver)((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).get("queryReceiver");
		QueryVector result = qr.getQuery(query);
		if (result==null) result=new QueryVector();
		return result;
	}	
	/**
	 * Removes the query result stored in the queryReceiver of your session. 
	 * @param soapParams <code>query</code> string of a query, It uses a {@link QueryReceiver} that is initiated on the session
	 * creation (sessionInitiated on Ewisdom class).
	 * @return a vector of queries
	 * @throws UserException
	 * @throws DHTException
	 * @throws FileException
	 * @throws CorruptIndexException
	 * @throws LockObtainFailedException
	 * @throws IOException
	 * @throws ParseException
	 */
	public XMLSerializable removeQueryResults(HashMap soapParams)  {

		String query=(String)soapParams.get("query");

		QueryReceiver qr = (QueryReceiver)((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).get("queryReceiver");
		QueryVector result = qr.getQuery(query);
		qr.removeQuery(query);
		if (result==null) result=new QueryVector();
		return result;
	}
	
	/**
	 * gets the fileDescriptor.
	 * @param soapParams <code>FID</code> FID, fileDescriptor identification
	 * @return a FileDescriptor
	 * @throws UserException
	 * @throws DHTException
	 * @throws FileException
	 */
	public XMLSerializable getFileProperties(HashMap soapParams) throws 	UserException, 
																			DHTException, 
																			FileException
	{
		String fid=(String)soapParams.get("FID");
		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

		FileDescriptor fd=(FileDescriptor)dhtFiles.get(fid); //param title
		if (fd==null) throw new FileException("File "+fid+" does not exist");
		checkRoules(userName,fd,"READ_CONTENTS");
		return fd;
	}	
	
	
	/**
	 * Rebuild the physical File from the fileParts of a version
	 * @param soapParams soapParams <code>FID</code> identification of a fileDescriptor, <code>version</code> name of a version "ver_x"
	 * @return a fileLocator where we will be able to download then file
	 * @throws UserException
	 * @throws ContentException
	 * @throws DHTException
	 * @throws FileException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public XMLSerializable restoreFile(HashMap soapParams) throws 	UserException, 
																ContentException, 
																DHTException, 
																FileException, 
																IOException,
																InterruptedException{
		String fid=(String)soapParams.get("FID");
		String ver = (String)soapParams.get("version");
		String userName = ((SOAPSession)soapParams.get(SOAPProxy.SOAPSession)).getUserName();
		DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

		FileDescriptor fd=(FileDescriptor)dhtFiles.get(fid); //param title

		checkRoules(userName,fd,"ADD_CONTENTS");
		FilePart[]fp=fd.getFileParts(ver);
		int v=Integer.parseInt(ver.substring(ver.lastIndexOf('_')+1));
		fd.setVer(v+1);//increments version number because fileDownloades decrements it
		for (int i=0;i<fp.length;i++){
			FileLocator chossedFl=chooseAServer(fp[i]);
			createThreadToDownload(fd,fp[i],chossedFl);
		}
			

		FileLocator fploc = new FileLocator();
		String host=getLocalAddress();
		fploc.setHostName(host);
		fploc.setPort(SERVER_PORT);
		fploc.setUrl("http://"+host+":"+SERVER_PORT+HTTP_SERVER_FOLDER+fd.getPath()+"/"+fd.getFileName());
		return fploc;
	}
		
	/**
	 * checks if the physical file of a FileDescriptor exists
	 * @param fd a FileDescriptor
	 * @return true if it exists, false if not.
	 */
	private boolean fileExists(FileDescriptor fd) {
		String path =REPOSITORY+fd.getPath()+File.separator+fd.getFileName();
		path=path.replace("/", File.separator);
		File file=new File(path);
		return file.exists();
	}

	/**
	 * Returns a FileLocator with the best ping time to download a FilePart
	 * @param filePart
	 * @return a FileLocator
	 */
	private FileLocator chooseAServer(FilePart filePart) {
		Object[]loc=filePart.getLocators();
		Ping p= new Ping();
		int bestPing=0;
		long bestTime=1000000000,ptime;
		for (int i=0;i<loc.length;i++){
			p.doPing(((FileLocator)loc[i]).getHostName());
			if ((ptime=p.getDuration())<bestTime){
				bestPing=i;	
				bestTime=ptime;
			}
		}
		return ((FileLocator)loc[bestPing]);
	}
	
	/**
	 * Creates an EwisdomDownloader object that creates a new thread listening the first free port for uploading a file purpose
	 * it registers itself as a FileUploadedListener. 
	 * @param fd
	 * @return a free port where the server listens.
	 * @throws IOException
	 * @throws InterruptedException
	 */
	private synchronized int createThreadToUpload(FileDescriptor fd) throws IOException, InterruptedException {
		// return first free port
		int i=0;
		while (uploaders[i]!=null) {
			i=(i<99)?i+1:0;
			if (i==0) this.wait(2000);
		}
		String path =(REPOSITORY+fd.getPath()+File.separator).replace("/", File.separator);
		if (i!=99){
			
			EwisdomUploader eup = new EwisdomUploader(FIRST_AVAILABLE_PORT+i,fd,path);			
			eup.addFileUploadedListener(this);
			uploaders[i]=eup;
			eup.start();
			return FIRST_AVAILABLE_PORT+i;
		}
		return -1;
		
	}


	/**
	 * Creates a new EwisdomDownloader object to download a FilePart from a FileLocator.
	 * It registers itself as a FileDownloadedListener.
	 * @param fd the FileDescriptor of a FilePart
	 * @param fp a FilePart to be downloaded.
	 * @param fl a FileLocator where it can download a FilePart.
	 * @return an index from the downloaders array
	 * @throws IOException
	 * @throws InterruptedException
	 */
	private synchronized int createThreadToDownload(FileDescriptor fd,FilePart fp,FileLocator fl) throws IOException, InterruptedException {
		// return first free port
		int i=0;
		while (downloaders[i]!=null) {
			i=(i<99)?i+1:0;
			if (i==0) this.wait(2000);
		}
		if (i!=99){
			//Downloads fileParts
			String destFolder=(REPOSITORY+
							  fd.getPath()+
							  File.separator).replace("/", File.separator);
			EwisdomDownloader edw = new EwisdomDownloader(fd,destFolder,fp.getFilePartName(),fl.getUrl());	
			edw.addFileDownloadedListener(this);
			downloaders[i]=edw;
			edw.start();
			return i;
		}
		return -1;
		
	}
	

	public void fileUploaded(FileUploadedEvent event) {
		try {
			String fid=((EwisdomUploader)event.getFileUploader()).getFID();
			DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));
			FileDescriptor fd = (FileDescriptor)dhtFiles.get(fid);
			
			fd.setFileName(event.getfileName());
			String destFolder=fd.getPath().replace("/", File.separator);
			String sourceFile = REPOSITORY+destFolder+File.separator+event.getfileName();
			//Creates fileParts and updates its urls
			
			FileSplitter fs = new FileSplitter(
					//Source file
					sourceFile,
					//Destination directory of file parts
					REPOSITORY+destFolder+File.separator,
					//Size of parts
					CHUNK_SIZE,
					//file descriptor
					fd,
					//accessible address
					getLocalAddress(),
					//accessible http port
					SERVER_PORT,
					//accessible http serrver address
					HTTP_SERVER_FOLDER+destFolder+'/');
			fs.start();
			//Indexes the pdf file
			//TODO index only pdfs!!!!!!
			File fileToIndex = new File(sourceFile);
			LuceneIndexer lind=new LuceneIndexer();
			lind.addDocument(fileToIndex,fd);
			//Updates the FileLocator
			FileLocator fploc = new FileLocator();
			String host=getLocalAddress();
			fploc.setHostName(host);
			fploc.setPort(SERVER_PORT);
			fploc.setUrl("http://"+host+":"+SERVER_PORT+HTTP_SERVER_FOLDER+fd.getPath()+"/"+fd.getFileName());
			fd.setMainNode(fploc);
			fs.join();
			dhtFiles.put(""+fd.hashCode(), fd);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	

	public void fileDownloaded(FileDownloadedEvent event) {
		try {
			String name=event.getFileDownloader().getName();
			FileDescriptor fd_old=((EwisdomDownloader)event.getFileDownloader()).getFileDescriptor();
			//Get the last FD
			DHTHandler dhtFiles = PastryKernel.getDHTHandler(Utilities.generateStringHash("p2p://org.jma.app.ewisdom.files"));

			FileDescriptor fd = (FileDescriptor)dhtFiles.get(name);
			// Version number requested is lower than the last version number 
			boolean newVersion=true;
			if (fd.getVer()-1 >= fd_old.getVer()) {
				fd=fd_old;
				newVersion=false;
			}
			
			
			FilePart[] fp=fd.getFileParts( "ver_" +(fd.getVer()-1));
			String destFolder=fd.getPath().replace("/", File.separator);
			for (int i=0;i<fp.length;i++){
				File file=new File(REPOSITORY+destFolder+File.separator+fp[i].getFilePartName());
				if (file.exists()){
					FileLocator fploc = new FileLocator();
					String host=getLocalAddress();
					fploc.setHostName(host);
					fploc.setPort(SERVER_PORT);
					fploc.setUrl("http://"+host+":"+SERVER_PORT+'/'+HTTP_SERVER_FOLDER+'/'+destFolder+'/'+fp[i].getFilePartName());
					fp[i].addLocator(fploc);
					if (newVersion) dhtFiles.put(""+fd.hashCode(), fd);
				} else {
					return; //there are not all the parts yet
				}
			}
			
			
			
			FileJoiner fj = new FileJoiner(
					//Destination folder
					REPOSITORY+destFolder,
					//file descriptor
					fd,
					//version
					String.valueOf((fd.getVer()-1))
					);
			fj.start();
			
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Return a valid address of the local node 
	 * @return a HostAddress
	 * @throws UnknownHostException
	 */
	private String getLocalAddress() throws UnknownHostException{
		InetAddress   in  = InetAddress.getLocalHost();
		InetAddress[] all = InetAddress.getAllByName(in.getHostName());
		for (int i=0; i<all.length; i++) {
			if (!all[i].isSiteLocalAddress()) 
				return all[i].getHostAddress();
		}
		return in.getHostAddress();
	}
}